
package com.hockeo.tinygds;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.hockeo.tinygds.annotation.TDiscriminatorValue;
import com.hockeo.tinygds.annotation.TEntity;
import com.hockeo.tinygds.annotation.TId;
import com.hockeo.tinygds.annotation.TParent;
import com.hockeo.tinygds.annotation.TUnindexed;
import com.hockeo.tinygds.error.TNotLoadableException;
import com.hockeo.tinygds.error.TNotPersistableException;
import com.hockeo.tinygds.error.TinyGDSException;

/**
 * Represents the meta data for a persistent entity class.
 *
 * @param <T> the class for which this metadata is intended for
 * @version $Id$
 * @author jjanke
 */
final class TMetaData<T>
{
  /**
   * We do not persist fields with any of these modifiers.
   */
  private static final int       BAD_MODIFIERS          = Modifier.FINAL | Modifier.STATIC | Modifier.TRANSIENT;

  /**
   * Default name that indicates that the simple class name should be used as entity name.
   */
  private static final String    DEFAULT_ENTITY_NAME    = "";

  /**
   * Indicates the name of the discriminator property.
   */
  static final String            DISCRIMINATOR_PROPERTY = "__tgds_discriminator";

  private final TinyGDSFactory   d_factory;
  private final Class<T>         d_entityClass;
  private Constructor<T>         d_constructor;
  private String                 d_strKind;
  private String                 d_strDiscriminator;
  private TFieldPrimaryKey<?>    d_fieldId;
  private TFieldKey              d_fieldParent;
  private Map<String, TField<?>> d_mapFields            = new HashMap<String, TField<?>>();
  private List<String>           d_listDiscriminatorsSameHierarchy;

  /**
   * Creates a new meta data instance for the given persistable class.
   *
   * @param factory the factory that created this object
   * @param cls the class object for which to construct the meta data instance
   * @throws TNotPersistableException if the given class is not annotated with
   *           {@link TEntity}
   */
  TMetaData( TinyGDSFactory factory, Class<T> cls )
  {
    if ( !cls.isAnnotationPresent( TEntity.class ) )
      throw TNotPersistableException.classNotAnnotatedWithEntity( cls );

    d_factory = factory;
    d_entityClass = cls;

    // Determine the discriminator value which is used to differenciate between different
    // classes that are part of the same inheritance structure and therefore share a
    // common kind. If no discriminator value is specified, the class' simple name is used
    // as default.
    //
    TDiscriminatorValue discriminatorVal = cls.getAnnotation( TDiscriminatorValue.class );
    if ( discriminatorVal != null && !discriminatorVal.value().equals( DEFAULT_ENTITY_NAME ) )
      d_strDiscriminator = discriminatorVal.value();
    else
      d_strDiscriminator = d_entityClass.getSimpleName();

    // get hold of the constructor to be used for object instantiation
    //
    d_constructor = getConstructor( d_entityClass );
    if ( d_constructor == null )
      throw TNotPersistableException.noArgumentLessConstructor( d_entityClass );

    inspect( d_entityClass );

    // check that there is an ID field, if not throw exception
    //
    if ( d_fieldId == null )
      throw TNotPersistableException.noIdField( cls );
  }

  /**
   * Returns the class object for the underlying entity.
   */
  Class<T> getEntityClass()
  {
    return d_entityClass;
  }

  /**
   * Returns a class object representing the parent entity class if this class has a field
   * annotated with {@link TParent}. Otherwise, returns <code>null</code>.
   */
  Class<?> getParentEntityClass()
  {
    if ( d_fieldParent == null )
      return null;

    return d_fieldParent.getReferencedType();
  }

  /**
   * Returns the kind of the represented entity class.
   */
  String getKind()
  {
    return d_strKind;
  }

  /**
   * Returns the discriminator of the represented entity class. Each class in an
   * inheritance hierarchy has its own descriminator value while the kind is the same for
   * all classes.
   */
  String getDiscriminator()
  {
    return d_strDiscriminator;
  }

  /**
   * Indicates if the class described by this metadata object is the top level class of
   * its inheritance hierarchy or not.
   */
  boolean isRootOfHierarchy()
  {
    return d_strKind.equals( d_strDiscriminator );
  }

  /**
   * Indicates if the described entity has a @TParent field or not.
   */
  boolean hasParentField()
  {
    return d_fieldParent != null;
  }

  /**
   * Creates a raw key for the given key value. The key will be targeted at the class this
   * metadata describes.
   *
   * @param objKeyValue the key value
   * @return the raw key matching the given key value
   */
  Key createRawKey( Object objKeyValue )
  {
    if ( d_fieldId instanceof TFieldPkString )
    {
      String strName = objKeyValue instanceof String ? (String) objKeyValue : String.valueOf( objKeyValue );

      return KeyFactory.createKey( d_strKind, strName );
    }
    else
    {
      if ( !( objKeyValue instanceof Long ) )
        throw new IllegalArgumentException( "The key value '" + String.valueOf( objKeyValue )
            + "' is not a valid Long although the entity '" + d_entityClass.getName() + "' has a Long/long @TId field." );

      return KeyFactory.createKey( d_strKind, (Long) objKeyValue );
    }
  }

  /**
   * Returns a list of all the discriminators that are part of the same inheritance tree
   * than the class represented by this TMetaData object.
   */
  List<String> getDiscriminatorsOfSameHierarchy()
  {
    if ( d_listDiscriminatorsSameHierarchy == null )
    {
      List<TMetaData<? extends T>> list = d_factory.getMetaDataByHierarchy( this );

      d_listDiscriminatorsSameHierarchy = new ArrayList<String>( list.size() );
      for ( TMetaData<? extends T> metadata : list )
        d_listDiscriminatorsSameHierarchy.add( metadata.getDiscriminator() );
    }

    return d_listDiscriminatorsSameHierarchy;
  }

  /**
   * Converts a persistent user code instance into a Google Datastore {@link Entity}
   * object.
   *
   * @param obj the object to be transformed into a Google entity
   * @return the transformed Google entity
   */
  Entity toEntity( T obj )
  {
    Entity ent = createEntity( obj );

    for ( String strProperty : d_mapFields.keySet() )
    {
      TField<?> field = d_mapFields.get( strProperty );
      Object val = field.getValueForDatastore( obj );

      if ( field.getRealField().isAnnotationPresent( TUnindexed.class ) )
        ent.setUnindexedProperty( strProperty, val );
      else
        ent.setProperty( strProperty, val );
    }

    return ent;
  }

  /**
   * Converts the given entity to an object of the type this meta data instance describes.
   * It is the responsability of the caller to make sure that this method is called on the
   * appropriate meta data instance.
   *
   * @param ent the entity which should be converted into a real POJO object
   * @return an object of the type represented by this meta data object
   */
  @SuppressWarnings( "unchecked" )
  T toObject( Entity ent )
  {
    // check kind
    //
    if ( !ent.getKind().equals( d_strKind ) )
      throw TNotLoadableException.invalidKind( ent, d_entityClass, d_strKind );

    // check discriminator (if the discriminator does not match, we have to ask another
    // metadata to create the requested object)
    //
    String strDiscriminator = (String) ent.getProperty( DISCRIMINATOR_PROPERTY );
    if ( strDiscriminator == null )
      throw TNotLoadableException.missingDiscriminator( ent );

    if ( !strDiscriminator.equals( d_strDiscriminator ) )
      return ( (TMetaData<? extends T>) d_factory.getMetaDataByDescriminator( strDiscriminator ) ).toObject( ent );

    T instanceNew = createNewInstance();

    // set key
    //
    setKey( instanceNew, ent.getKey() );

    // set other fields
    //
    for ( Map.Entry<String, Object> prop : ent.getProperties().entrySet() )
    {
      // skip discriminator
      //
      if ( prop.getKey().equals( DISCRIMINATOR_PROPERTY ) )
        continue;

      TField<?> field = d_mapFields.get( prop.getKey() );

      field.setValueFromDatastore( instanceNew, prop.getValue() );
    }

    return instanceNew;
  }

  /**
   * Creates a new uninitialised instance of the class this metadata object represents.
   */
  T createNewInstance()
  {
    try
    {
      return d_constructor.newInstance();
    }
    catch ( InvocationTargetException ex )
    {
      throw new TinyGDSException( ex.getCause() );
    }
    catch ( Exception ex )
    {
      throw new TinyGDSException( ex );
    }
  }

  /**
   * Sets the given keys (id and parent) to the specified object instance.
   *
   * @param instance the instance to which to set the key
   * @param key the key to be used for setting the instance's Id property
   */
  void setKey( T instance, Key key )
  {
    // id
    //
    d_fieldId.setKey( instance, key );

    // parent key
    //
    Key keyParent = key.getParent();
    if ( keyParent != null )
    {
      if ( d_fieldParent == null )
        throw TNotLoadableException.noParentField( key, d_entityClass );

      d_fieldParent.setValue( instance, d_factory.rawKeyToTKey( keyParent, d_fieldParent.getReferencedType() ) );
    }
  }

  /**
   * Returns a key that matches the object's id and references its parent (if it has one).
   *
   * @param instance the instance for which to retrieve the key
   * @return the key matching the given instance
   * @throws TinyGDSException if the given object has no set @Id
   */
  Key getKey( Object instance )
  {
    if ( !d_entityClass.isAssignableFrom( instance.getClass() ) )
      throw new TinyGDSException( "Cannot get key for instance '" + instance.toString() + "' of type '"
          + instance.getClass().getName() + "' with the metadata for the class '" + d_entityClass.getName() + "'." );

    Key parent = null;

    // parent key
    //
    if ( this.d_fieldParent != null )
      parent = d_factory.tKeyToRawKey( d_fieldParent.getValue( instance ) );

    // string id
    //
    return d_fieldId.getKey( instance, d_strKind, parent );
  }

  /**
   * <p>
   * Tries to retrieve the argument-less constructor of the class. First it tries to get
   * hold of any declared constructor directly declared in the given class. If the
   * constructor is non-public, {@link Constructor#setAccessible(boolean)} is called to
   * make it accessible. If there is no declared constructor, we check the whole class
   * hierarchy for a reachable argument-less constructor.
   * </p>
   *
   * <p>
   * Using an accessible private constructor is much more performant than a public
   * constructor. Therefore this method always checks first if such a constructor is
   * available.
   * </p>
   *
   * @param cls the class for which to retrieve a constructor
   * @return a matching constructor object or <code>null</code> if there is none available
   */
  private Constructor<T> getConstructor( Class<T> cls )
  {
    Constructor<T> c;

    try
    {
      c = cls.getDeclaredConstructor();

      if ( ( c.getModifiers() & Modifier.PUBLIC ) == 0 )
        c.setAccessible( true );
    }
    catch ( SecurityException ex )
    {
      throw new TinyGDSException( ex );
    }
    catch ( NoSuchMethodException ex )
    {
      // we will try finding a constructor that is possibly defined in a super class
      //
      try
      {
        c = cls.getConstructor();
      }
      catch ( SecurityException ex1 )
      {
        throw new TinyGDSException( ex );
      }
      catch ( NoSuchMethodException ex1 )
      {
        // no matching constructor found
        //
        return null;
      }
    }

    return c;
  }

  /**
   * Recursively inspects the given class extracting the kind as well as all persistable
   * fields.
   *
   * @param cls the class for which this TMetaData instance is intended for
   */
  private void inspect( Class<? super T> cls )
  {
    // condition to stop recursion
    //
    if ( cls == null || cls == Object.class )
      return;

    // If it is an Entity, we set the kind. As the method is called recursively from the
    // bottom to the top of the inheritance hierarchy, we are sure that at the end the
    // top-most class annotated with Entity determines the kind.
    //
    TEntity entity = cls.getAnnotation( TEntity.class );
    if ( entity != null )
      d_strKind = entity.name().equals( DEFAULT_ENTITY_NAME ) ? cls.getSimpleName() : entity.name();

    // Get all the relevant fields incl. the ID (which may be part of a super class)
    //
    for ( Field field : cls.getDeclaredFields() )
    {
      // skip final, static, transient fields
      //
      if ( ( field.getModifiers() & BAD_MODIFIERS ) != 0 )
        continue;

      if ( field.isAnnotationPresent( TId.class ) )
      {
        // Check if we already have an Id field, if yes - than we have a problem. We
        // actually provide the original entity class to the exception as that is the
        // class whose hierarchy we are examining (and there must not be more than one Id
        // in the whole hierarchy).
        //
        if ( d_fieldId != null )
          throw TNotPersistableException.tooManyIds( d_entityClass );

        if ( field.getType() == Long.TYPE || field.getType() == Long.class )
          d_fieldId = new TFieldPkLong( d_factory, field );
        else if ( field.getType() == String.class )
          d_fieldId = new TFieldPkString( d_factory, field );
        else
          throw TNotPersistableException.wrongIdType( field );
      }
      else if ( field.isAnnotationPresent( TParent.class ) )
      {
        if ( d_fieldParent != null )
          throw TNotPersistableException.tooManyParents( d_entityClass );

        if ( field.getType() == TKey.class )
          d_fieldParent = new TFieldKey( d_factory, field );
        else
          throw TNotPersistableException.wrongParentType( field );
      }
      else
      {
        TField<?> tfield = TField.createTField( d_factory, field );

        if ( d_mapFields.put( tfield.getPropertyName(), tfield ) != null )
          throw TNotPersistableException.tooManyPropertiesWithSameName( d_entityClass, tfield.getPropertyName() );
      }

      // make non-public fields accessible
      //
      if ( ( field.getModifiers() & Modifier.PUBLIC ) == 0 )
        field.setAccessible( true );
    }

    // continue with super class
    //
    inspect( cls.getSuperclass() );
  }

  /**
   * <p>
   * Creates and initialises a new {@link Entity} instance. Handles the following cases:
   * </p>
   *
   * <ul>
   * <li>The object has a String or a long id.</li>
   * <li>The object has an unitialised Long id that requires an auto-generated value.</li>
   * <li>The object may have a parent key.</li>
   * </ul>
   *
   * @param instance the object for which an entity should be initialised
   * @return a new initialised Entity object
   */
  private Entity createEntity( T instance )
  {
    Entity ent;
    Key keyParent = null;

    // parent key
    //
    if ( d_fieldParent != null )
      keyParent = d_factory.tKeyToRawKey( d_fieldParent.getValue( instance ) );

    // new entity
    //
    ent = d_fieldId.createEntity( instance, d_strKind, keyParent );

    // set discriminator property
    //
    ent.setProperty( DISCRIMINATOR_PROPERTY, d_strDiscriminator );

    return ent;
  }
}
